#!/usr/bin/env python
#
# Copyright 2008 Ilya Novoselov
#
# This file is part of TyuChat.
#
# TyuChat is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# TyuChat is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with TyuChat.  If not, see <http://www.gnu.org/licenses/>.

import os
import time
import datetime
import random
import string
from itertools import imap, repeat
import socket
import re

from twisted.application import service
from twisted.internet import protocol, reactor
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.error import ConnectionDone
# from twisted.python import log
from twisted.web2 import static, http, responsecode
from twisted.words.protocols import irc

import twistedpie

message_timeout = 60;

def mkisotimestamp(timestamp):
    """Encode iso timestamp"""  
    return datetime.datetime.fromtimestamp(timestamp).isoformat()

def isotimestamp(isotimestamp):
    """Decode ISO timestamp"""   
    return time.mktime(time.strptime(isotimestamp, '%Y-%m-%dT%H:%M:%SZ'))

def generate_ticket():
    """Generate chat ticket"""
    return u"".join(imap(random.choice,
                        repeat(string.digits + string.ascii_letters, 32)))

        
class IRCConnection(irc.IRCClient):        
    def connectionMade(self):
        irc.IRCClient.connectionMade(self)
        self.webConnection.ircConnectionMade(self)
    
    def connectionLost(self, reason):
        irc.IRCClient.connectionLost(self, reason)
        self.webConnection.ircConnectionLost(reason)
    
    def signedOn(self):
        """Called when client has succesfully signed on to server."""
        self.reportEvent('signedOn')
        # Also report nickChange event, because nick may have changed during
        # registration.
        if self.nickname != self.factory.nickname:
            self.reportEvent('nickChanged', nick=self.nickname)
        
        self.webConnection.ircSignedOn()
    
    def reportEvent(self, type, **attributes):
        self.webConnection.newEncodedEvent(type, **attributes)
    
    def privmsg(self, user, channel, msg):
        """This will get called when the client receives a message."""
        self.reportEvent('privmsg', user=user, channel=channel, msg=msg)
    
    def noticed(self, user, channel, msg):
        """Notice received"""
        self.reportEvent('notice', user=user, channel=channel, msg=msg)
        
    def action(self, user, channel, data):
        self.reportEvent('action', user=user, channel=channel, data=data)
    
    # These events affect user list.
    def userJoined(self, user, channel):
        self.reportEvent('join', user=user, channel=channel)
    
    def userKicked(self, kickee, channel, kicker, message):
        self.reportEvent('kick', kickee=kickee, channel=channel, kicker=kicker,
                         message=message)
    
    def userLeft(self, user, channel):
        self.reportEvent('leave', user=user, channel=channel)
    
    def userQuit(self, user, quitMessage):
        self.reportEvent('quit', user=user, quitMessage=quitMessage)
    
    def userRenamed(self, oldname, newname):
        self.reportEvent('nick', oldname=oldname, newname=newname)
    
    # This events affect my state.
    def joined(self, channel):
        self.reportEvent('joined', channel=channel)
    
    def nickChanged(self, nick):
        self.reportEvent('nickChanged', nick=nick)
    
    def msg(self, target, msg):
        irc.IRCClient.msg(self, target, msg)
        
    def irc_RPL_NAMREPLY(self, prefix, params):
        channel = params[2];
        names = params[3].strip().split(" ")
        
        reply = []
        
        for name in names:
            if name[0] in "@%+&~":
                reply.append(dict(mode=name[0], nickname=name[1:]))
            else:
                reply.append(dict(mode=None, nickname=name))    

        self.reportEvent('names', channel=channel, names=reply)
    
    def irc_ERROR(self, prefix, params):
        """Handle ERROR message from server"""
        self.reportEvent('error', message=params[0])


class IRCConnectionFactory(protocol.ClientFactory):
    protocol = IRCConnection
    
    def __init__(self, webConnection, nickname, username):
        self.webConnection = webConnection
        self.nickname = nickname
        self.username = username
    
    def buildProtocol(self, addr):
        p = self.protocol()
        p.webConnection = self.webConnection
        p.nickname = self.nickname
        p.username = self.username
        p.factory = self
        return p


class WebConnection(object):
    """This class represents instance of user interface, identified by ticket"""
    
    # FIXME: so low intervals are for debugging purposes.
    # How long to wait for client request for getEvents before considering
    # disconnect.
    requestEventsTimeout = 60.0
    # How long to wait for new event before returning empty list.
    waitEventsTimeout = 60.0
  
    def __init__(self, dispatcher, ticket, nickname, username, server=None,
                 charset=None, channel=None):
        self.eventRequest = None
        self.eventRequestTimer = None
        self.eventCounter = 0
        self.events = []
        self.ircConnection = None
        self.disconnectTimer = None

        self.ticket = ticket
        self.dispatcher = dispatcher
        
        self.defaultNickname = nickname
        self.charset = charset
        self.username = username
        self.server = server
        self.defaultChannel = channel
        
        self.setDisconnectTimer()
        
        # Connect to server
        self.connect()
    
    def getNickname(self):
        if self.ircConnection:
            return self.ircConnection.nickname.decode(self.charset)
        else:
            return self.defaultNickname

    def connect(self):
        f = IRCConnectionFactory(self,
                                 self.defaultNickname.encode(self.charset),
                                 self.username)
        reactor.connectTCP(self.server, 6667, f)

    def setDisconnectTimer(self):
        assert(not self.disconnectTimer or not self.disconnectTimer.active())
        self.disconnectTimer = reactor.callLater(self.requestEventsTimeout,
                                                 self.loseConnection, 'timeout')
    
    def ircConnectionMade(self, connection):
        """Called when IRC connection is established"""
        self.ircConnection = connection
    
    def ircConnectionLost(self, reason):
        """Called when IRC connection is lost"""
        self.ircConnection = None
        if(isinstance(reason.value, ConnectionDone)):
            self.newEvent('disconnect')
        else:
            self.newEncodedEvent('connectionLost',
                                 reason=reason.getErrorMessage())
    
    def ircSignedOn(self):
        if(self.defaultChannel):
            self.handleCommand(None, 'join', self.defaultChannel)
    
    def loseConnection(self, reason):
        """Called when web client connection is about to be lost"""
        if self.ircConnection:
            self.ircConnection.quit(reason)
        self.dispatcher.connectionLost(self.ticket, reason)
        
    # Regexp for cmd detection.
    cmdRegexp = re.compile(ur"^/\s*(\w+)\s*(.*)$", re.U)
    
    def handleInput(self, target, input):
        """Handle user input"""
        
        if not input:
            # Empty string, do nothing.
            return
        
        match = self.cmdRegexp.match(input)
        
        if match:
            # Command.
            cmd, args = match.groups()
            self.handleCommand(target, cmd, args)
        else:
            self.handleCommand(target, u"msg", input)
                
    def handleCommand(self, target, cmd, args):
        if self.ircConnection:
            if cmd == u"quit":
                self.ircConnection.quit(args.encode(self.charset))
            elif cmd == u"join":
                self.ircConnection.join(args.encode(self.charset))
            elif cmd == u"me":
                self.ircConnection.me(target.encode(self.charset),
                                      args.encode(self.charset))
                self.newEvent('action',
                              user=self.getNickname(),
                              channel=target, data=args)
            elif cmd == u"msg":
                self.ircConnection.msg(target.encode(self.charset),
                                       args.encode(self.charset))
                self.newEvent('privmsg',
                              user=self.getNickname(),
                              channel=target, msg=args)
        else:
            if cmd == u"connect":
                self.connect()
    
    def decodeStruct(self, s):
        if isinstance(s, dict):
            return dict((k, self.decodeStruct(v)) for k, v in s.iteritems())
        elif isinstance(s, list):
            return [self.decodeStruct(i) for i in s]
        elif isinstance(s, str):
            return s.decode(self.charset)
        elif s is None:
            return None
        else:
            raise Exception("Can't decode type %s" % type(s))
    
    def newEncodedEvent(self, type, **encoded):
        """Decodes attributes and calls newEvent"""
        attributes = self.decodeStruct(encoded)
        return self.newEvent(type, **attributes)

    def newEvent(self, type, **attributes):
        """Register new event, fire callbacks"""
        
        event = attributes.copy()

        event['type'] = type
        event['id'] = self.eventCounter
        self.eventCounter += 1
        event['timestamp'] = mkisotimestamp(time.time())
        self.events.append(event)
        
        self._finishRequest([event])     
            
    def _finishRequest(self, events):
        """Notify requester and cancel request timer"""
        
        assert(self.disconnectTimer)

        # Notify requester (if one).
        if self.eventRequest:
            # So, request is active. And disconnectTimer is canceled too.
            assert(not self.disconnectTimer.active())
            
            self.eventRequest.callback(events)
            self.eventRequest = None
        
            # Reset disconnect timer.
            self.setDisconnectTimer()
        
        # Cancel request timer, if it is active.
        if self.eventRequestTimer and self.eventRequestTimer.active():
            self.eventRequestTimer.cancel()
            self.eventRequestTimer = None
                        
    def getEvents(self, lastId):
        # We don't want concurrent requests from two clients.
        self._finishRequest([])
        
        # Get old events.
        events = self.getOldEvents(lastId)
        
        if events:
            # Reset disconnect timer. It must be active, because we finished
            # previous deferred request.
            assert(self.disconnectTimer.active())
            self.disconnectTimer.reset(self.requestEventsTimeout)
            # Don't mess with wait timer, return.
            return events
        
        # So, we are going to wait for new events. Cancel disconnect timer.
        assert(self.disconnectTimer.active())
        self.disconnectTimer.cancel()
        
        # Wait for new event.
        self.eventRequest = Deferred()

        # Set request timer.
        self.eventRequestTimer = reactor.callLater(self.waitEventsTimeout,
                                                    self._finishRequest, [])
        
        # Return our deferred result.
        return self.eventRequest

    def getOldEvents(self, lastId=None):
        """Returns recent events immediately"""

        # Retrieve new events
        newEvents = []
        for event in self.events:
            if event['id'] > lastId:
                newEvents.append(event)

        return newEvents

class WebDispatcher(object):
    responseFactory = twistedpie.ResponseFactory(nocache=True)
  
    def __init__(self):
        self.clients = {}

    @twistedpie.expose 
    def index(self):
        charsets = "cp1251 koi8-r utf-8".split(" ")
        return self.responseFactory.render('index.html', charsets=charsets)
    
    @twistedpie.expose
    def connect(self, nickname, server="irc.tgasu.ru", charset="cp1251",
                channel="#tgngu"):
        ticket = generate_ticket()
        
        address = twistedpie.req.chanRequest.getRemoteHost().host
        username = str("%02x" * 4 % tuple(ord(c) for c in socket.inet_aton(address)))
        
        client = WebConnection(self, ticket, nickname, username, server,
                                  charset, channel)
        
        self.clients[ticket] = client
        
        return self.responseFactory.render('chat.html', nickname=nickname, 
                                           ticket=ticket, server=server,
                                           channel=channel)
        
    def connectionLost(self, ticket, reason):
        del self.clients[ticket]
    
    @twistedpie.expose
    def submitInput(self, ticket, target, input):
        client = self.clients[ticket]
        
        client.handleInput(target, input)
        
        return self.responseFactory.empty()
    
    @twistedpie.expose
    def getEvents(self, ticket, lastId=None):
        '''
        Retrieve events from event list
        @param lastId: start from this id
        '''
        
        if ticket not in self.clients:
            return http.StatusResponse(responsecode.NOT_FOUND,
                                       "Specified ticket not found")

        if lastId:
            lastId = int(lastId)
        
        interface = self.clients[ticket]

        d = maybeDeferred(interface.getEvents, lastId)
        d.addCallback(self.responseFactory.json)
        return d
     
    
# Create the resource we will be serving
application = service.Application("chat")

staticDir = os.path.join(os.path.dirname(__file__), '..', 'static')

pie = twistedpie.Pie(WebDispatcher())
pie.mount('/static', static.File(staticDir))

s = pie.service(8080)
s.setServiceParent(application)